package edu.hawaii.ics314.ical.model;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;


/**
 * The CalendarComponent class represents the <code>Property</code> available to
 * create an event in a calendar. 
 * 
 * @author 
 *
 */
public abstract class CalendarComponent implements StringSerializable, Comparable<CalendarComponent> {
	
	public enum ComponentPriority { 
		UNDEFINED(0),
		A1_HIGH(1),
		A2(2),
		A3(3),
		B1_MEDIUM(4),
		B2(5),
		B3(6),
		C1_LOW(7),
		C2(8),
		C3(9);
		
		private final int INT_VALUE;
		ComponentPriority(int intValue) {
			INT_VALUE = intValue;
		}
		
		public int intValue() {
			return INT_VALUE;
		}
		
		@Override
		public String toString() {
			return INT_VALUE + "";
		}

		public static ComponentPriority parse(String priorityValue) throws ParseException {
			Objects.requireNonNull(priorityValue);
			try {
				// Try to parse it as an integer value first.
				int enumValIdx = Integer.parseInt(priorityValue);
				if(enumValIdx < 0 || enumValIdx > ComponentPriority.values().length) {
					throw new ParseException("the specified priority (" + enumValIdx 
							+ ") is outside of the valid range (0-9)", enumValIdx);
				} else {
					return ComponentPriority.values()[enumValIdx];
				}
			} catch(NumberFormatException e) {
				// If it is not an integer, try to parse it as the enum value name.
				return ComponentPriority.valueOf(priorityValue);
			}
		}
	}
	
	private final int PEEK_LINE_BUF_LEN = 1024;
	
	protected Calendar hostCalendar;
	protected Property<String> classification;
	protected Property<String> location;
	protected Property<ComponentPriority> priority;
	protected Property<String> summary;
	protected Property<DateTime> startDateTime;
	protected Property<DateTime> endDateTime;
	protected Property<TimeZone> timeZone;
	protected List<Property<?>> unrecognizedProperties;
	
	/**
	 * Constructor that creates a new <code>CalendarComponent</code> and 
	 * sets all properties to an initial value.
	 * 
	 * @param info - the initial state information of this <code>CalendarComponent</code>.
	 */
	public CalendarComponent(CalendarComponentInfo info) {
		hostCalendar = info.hostCalendar;
		classification = info.classification;
		location = info.location;
		priority = info.priority;
		summary = info.summary;
		startDateTime = info.startDateTime;
		endDateTime = info.endDateTime;
		timeZone = info.timeZone;
		unrecognizedProperties = (info.unrecognizedProperties == null ? new ArrayList<Property<?>>() : 
			new ArrayList<Property<?>>(Arrays.asList(info.unrecognizedProperties)));
	}
	
	/**
	 * Returns <code>classification</code>, or <code>null</code>.
	 * 
	 * @return <code>classification</code> or <code>null</code>.
	 */
	public String getClassification() {
		return (classification != null ? classification.getValue() : null);
	}
	
	/**
	 * Returns <code>location</code>, or <code>null</code>.
	 * 
	 * @return <code>location</code> or <code>null</code>.
	 */
	public String getLocation() {
		return (location != null ? location.getValue() : null);
	}
	
	/**
	 * Returns the <code>ComponentPriority</code> of this component, or <code>ComponentPriority.UNDEFINED</code>.
	 * 
	 * @return the <code>ComponentPriority</code> of this component, or <code>ComponentPriority.UNDEFINED</code>.
	 */
	public ComponentPriority getPriority() {
		return (priority != null ? priority.getValue() : ComponentPriority.UNDEFINED);
	}
	
	/**
	 * Returns the summary of this object, or <code>null</code>.
	 * 
	 * @return the summary of this object or <code>null</code>.
	 */
	public String getSummary() {
		return (summary != null ? summary.getValue() : null);
	}
	
	/**
	 * Returns the start date time of this calendar, or <code>null</code>.
	 * 
	 * @return the start date time of this calendar, or <code>null</code>.
	 */
	public DateTime getStartDateTime() {
		return (startDateTime != null ? startDateTime.getValue() : null);
	}
	
	/**
	 * Returns the end date time of this calendar, or <code>null</code>.
	 * 
	 * @return the end date time of this calendar, or <code>null</code>.
	 */
	public DateTime getEndDateTime() {
		return (endDateTime != null ? endDateTime.getValue() : null);
	}
	
	/**
	 * Returns the time zone of this object, or <code>null</code>.
	 * 
	 * @return the time zone of this object, or <code>null</code>.
	 */
	public TimeZone getTimeZone() {
		return (timeZone != null ? timeZone.getValue() : null);
	}
	
	/**
	 * Returns input file properties that were not recognized.
	 * 
	 * @return input file properties that were not recognized.
	 */
	public Property<?>[] getUnrecognizedProperties() {
		return (unrecognizedProperties != null ? 
				unrecognizedProperties.toArray(new Property<?>[unrecognizedProperties.size()]) : new Property<?>[0]);
	}
	
	@Override
	public int compareTo(CalendarComponent other) {
		if(getStartDateTime() == null && other.getStartDateTime() == null) {
			return 0;
		} else if(getStartDateTime() == null) {
			return (other.getStartDateTime() != null ? -1 : 0);
		} else if(other.getStartDateTime() == null) {
			return (getStartDateTime() != null ? 1 : 0);
		} else {
			return getStartDateTime().compareTo(other.getStartDateTime());
		}
	}
	
	/**
	 * From the current position of the specified <code>BufferedReader</code>, reads 
	 * properties until the end of the current <code>CalendarComponent</code> is reached.  
	 * 
	 * @param in - the specified <code>BufferedReader</code>.
	 * @param propRecognizer - recognizes and assigns <code>CalendarComponent</code> subclass-specific properties.
	 * @return Properties which were unable to be parsed.
	 * @throws IOException if an error occurs while reading from the specified <code>BufferedReader</code>.
	 * @throws ParseException if an error occurs while parsing a property.
	 */
	protected Property<?>[] readProperties(BufferedReader in, PropertyRecognizer propRecognizer) 
			throws IOException, ParseException {
		String curLine = null;
		do {
			curLine = peekNextLine(in);
			if(curLine.startsWith("BEGIN:") || curLine.startsWith("END:")) {
				break; // Avoid committing reads which are used by the property's parent.
			} else {
				in.readLine(); // Commit the read: move the reader cursor to the next line.
				Property<String> parsedProp = Property.parse(curLine);
				boolean isStdProp = tryAssignStdProp(parsedProp);
				if(!isStdProp && (propRecognizer == null || !propRecognizer.isRecognized(parsedProp))) {
					unrecognizedProperties.add(parsedProp);
				}
			}
		} while(curLine != null);
		return getUnrecognizedProperties();
	}
	
	/**
	 * Writes the event's properties to a <code>BufferedWriter</code>.
	 * 
	 * @param out - where the properties are being written to.
	 * 
	 * @return If all the properties were written or not.
	 * @throws IOException - if it cannot write to the current file or output stream.
	 */
	protected boolean writeProperties(BufferedWriter out) throws IOException {
		boolean propsWritten = false;
		if(classification != null) {
			out.write(classification.toString());
			propsWritten = true;
		}
		if(location != null) {
			out.write(location.toString());
			propsWritten = true;
		}
		if(priority != null) {
			out.write(priority.toString());
			propsWritten = true;
		}
		if(summary != null) {
			out.write(summary.toString());
			propsWritten = true;
		}
		if(startDateTime != null) {
			out.write(startDateTime.toString());
			propsWritten = true;
		}
		if(endDateTime != null) {
			out.write(endDateTime.toString());
			propsWritten = true;
		}
		if(timeZone != null) {
			out.write(timeZone.VALUE.toString());
			propsWritten = true;
		}
		for(Property<?> unrecognizedProp : unrecognizedProperties) {
			out.write(unrecognizedProp.toString());
			propsWritten = true;
		}
		return propsWritten;
	}
	
	/**
	 * Views what the next line is.
	 * 
	 * @param in - the buffer being read from.
	 * @return What the next line is.
	 * @throws IOException if an I/O error occurs on the next line.
	 */
	protected String peekNextLine(BufferedReader in) throws IOException {
		
		if(!in.ready()) {
			throw new IllegalArgumentException("the specified reader must be ready to read");
		} else if(!in.markSupported()) {
			throw new UnsupportedOperationException("the specified reader must support mark() and reset()");
		}
		in.mark(PEEK_LINE_BUF_LEN);
		String nextLine = in.readLine();
		in.reset();
		return nextLine;
	}
	
	/**
	 * Assigns a <code>Property</code> to this <code>CalendarComponent</code>.
	 * 
	 * @param property - the <code>Property</code> being assigned.
	 * @return <code>true</code> if the <code>Property</code> was assigned, <code>false</code> otherwise.
	 * @throws ParseException if the specified time zone is not a component of the host calendar.
	 * 
	 */
	private boolean tryAssignStdProp(Property<String> property) throws ParseException {
		Objects.requireNonNull(property);
		switch(property.getName()) {
		case "CLASSIFICATION":
			classification = property;
			return true;
		case "LOCATION":
			location = property;
			return true;
		case "PRIORITY":
			priority = new Property<ComponentPriority>(
					property.getName(), ComponentPriority.parse(property.getValue()), property.getParams());
			return true;
		case "SUMMARY":
			summary = property;
			return true;
		case "DTSTART":
			DateTime dtstart = DateTime.parse(property.getValue());
			startDateTime = new Property<DateTime>(property.getName(), dtstart, property.getParams());
			return true;
		case "DTEND":
			DateTime dtend = DateTime.parse(property.getValue());
			endDateTime = new Property<DateTime>(property.getName(), dtend, property.getParams());
			return true;
		case "TZID":
			TimeZone referencedTimeZone = null;
			if(hostCalendar != null) {
				for(CalendarComponent comp : hostCalendar.getCalendarComponents()) {
					if(comp instanceof TimeZone && ((TimeZone) comp).getID().equals(property.VALUE)) {
						referencedTimeZone = (TimeZone) comp;
						break;
					}
				}
			}
			if(this instanceof TimeZone) {
				timeZone = new Property<TimeZone>("TZID", ((TimeZone) this), property.getParams());
				return false;
			} else if(referencedTimeZone == null) {
				throw new ParseException(
						"the specified time zone ID has not been defined: " + property.toString(), -1);
			} else {
				timeZone = new Property<TimeZone>(property.getName(), referencedTimeZone, property.getParams());
				return true;
			}
		default:
			return false;
		}
	}
	
}
